/*
 * 源文件名：PaginationInterceptor.java
 * 文件版本：1.0.0
 * 创建作者：莫寅
 * 创建日期：2013-02-24
 * 修改作者：莫寅
 * 修改日期：2013-02-24
 * 文件描述：MyBatis物理分页插件
 * 版权所有：Copyright 2013 绍兴山耐, Inc. All Rights Reserved.
 */
package com.cloudbroker.bcs.platform.ltb.middle.mybatis;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

import javax.xml.bind.PropertyException;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cloudbroker.bcs.platform.ltb.middle.mybatis.dialect.Dialect;

/**
 * MyBatis物理分页插件.
 */
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {
		MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class PaginationInterceptor implements Interceptor, Serializable {

	/**
	 * serialVersionUID.
	 */
	private static final long serialVersionUID = -4420829689661146956L;

	/**
	 * Logger.
	 */
	private static Logger logger = LoggerFactory.getLogger(PaginationInterceptor.class);

	/**
	 * 方言类属性名.
	 */
	private static String DIALECT_CLASS = "dialectClass";

	/**
	 * 正则表达式属性名.
	 */
	private static String SQL_PATTERN = "sqlPattern";

	/**
	 * 需要拦截的ID值,支持正则表达式,可以XML中配置.
	 */
	protected String sqlPattern = "";

	/**
	 * 数据库方言.
	 */
	protected Dialect dialect;

	/**
	 * 插件物理分页功能,满足Mapper.xml的ID值符合sqlPattern的正则样式,并且有Pagination的上下文环境.
	 */
	public Object intercept(Invocation invocation) throws Throwable {

		// 取得MappedStatement对象
		final MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

		if (mappedStatement.getId().matches(sqlPattern) && PageContext.isExistPageContext()) {
			Object parameter = invocation.getArgs()[1];
			BoundSql boundSql = mappedStatement.getBoundSql(parameter);
			String originalSql = boundSql.getSql();
			Object parameterObject = boundSql.getParameterObject();

			// 判断SQL语句为空的情况下不执行
			if (StringUtils.isBlank(boundSql.getSql())) {
				return null;
			}

			// Pagination分页,根据上下文传参取得
			Pagination page = PageContext.getPageContext();

			// 取得总的记录条数
			Connection connection = mappedStatement.getConfiguration()
					.getEnvironment().getDataSource().getConnection();
			int totalRows = getCount(originalSql, connection,
					mappedStatement, parameterObject, boundSql);
			// 设置上下文环境的分页对象
			page.setTotalRows(totalRows);

			// 取得方言SQL
			String pageSql = generatePageSqlByDialect(originalSql, page, dialect);
			invocation.getArgs()[2] = new RowBounds(
					RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
			BoundSql newBoundSql = new BoundSql(
					mappedStatement.getConfiguration(), pageSql,
					boundSql.getParameterMappings(),
					boundSql.getParameterObject());
			MappedStatement newMs = copyFromMappedStatement(
					mappedStatement, new BoundSqlSqlSource(newBoundSql));

			invocation.getArgs()[0] = newMs;
		}

		return invocation.proceed();
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	/**
	 * 初始化取得属性值,取得Dialect Class和SQL Pattern.
	 */
	@SuppressWarnings("rawtypes")
	public void setProperties(Properties p) {
		// 实例化方言类
		String dialectClass = p.getProperty(DIALECT_CLASS);
		if (StringUtils.isEmpty(dialectClass)) {
			try {
				throw new PropertyException("dialectClass not found.");
			} catch (PropertyException e) {
				logger.error(e.getMessage());
			}
		} else {
			try {
				Class dialectCls = Class.forName(dialectClass);
				dialect = (Dialect) dialectCls.newInstance();
			} catch (ClassNotFoundException e) {
				logger.error("dialectClass not found.", e);
			} catch (InstantiationException e) {
				logger.error("dialectClass instance error.", e);
			} catch (IllegalAccessException e) {
				logger.error("dialectClass instance illegal.", e);
			}
		}

		// 取得需要拦截的正则
		sqlPattern = p.getProperty(SQL_PATTERN);
		if (StringUtils.isEmpty(sqlPattern)) {
			try {
				throw new PropertyException("sqlPattern not found!");
			} catch (PropertyException e) {
				logger.error(e.getMessage());
			}
		}

	}

	/**
	 * copyFromMappedStatement.
	 */
	private MappedStatement copyFromMappedStatement(MappedStatement ms,
			SqlSource newSqlSource) {
		MappedStatement.Builder builder = new MappedStatement.Builder(
				ms.getConfiguration(), ms.getId(), newSqlSource,
				ms.getSqlCommandType());
		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		if (ms.getKeyProperties() != null) {
			for (String keyProperty : ms.getKeyProperties()) {
				builder.keyProperty(keyProperty);
			}
		}
		builder.timeout(ms.getTimeout());
		builder.parameterMap(ms.getParameterMap());
		builder.resultMaps(ms.getResultMaps());
		builder.cache(ms.getCache());
		return builder.build();
	}

	/**
	 * 匿名BoundSqlSqlSource类.
	 */
	public static class BoundSqlSqlSource implements SqlSource {
		BoundSql boundSql;

		public BoundSqlSqlSource(BoundSql boundSql) {
			this.boundSql = boundSql;
		}

		public BoundSql getBoundSql(Object parameterObject) {
			return boundSql;
		}
	}

    /**
     * 取得总的数据条数.
     */
    public static int getCount(final String sql, final Connection connection,
                               final MappedStatement mappedStatement, final Object parameterObject,
                               final BoundSql boundSql) throws SQLException {
    	// 综合考虑使用count(*)
        final String countSql = "SELECT COUNT(*) as count FROM (" + sql + ") AS temp";
        PreparedStatement statement = null;
        ResultSet rs = null;
        try {
        	statement = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), parameterObject);
            setParameters(statement, mappedStatement, countBS, parameterObject);
            rs = statement.executeQuery();
            int count = 0;
            if (rs.next()) {
                count = rs.getInt("count");
            }
            return count;
        } finally {
            if (rs != null) {
                rs.close();
            }
            if (statement != null) {
            	statement.close();
            }
            if(connection != null){
            	connection.close();
            }
        }
    }

    /**
     * 根据方言,生成特定SQL语句.
     */
    public static String generatePageSqlByDialect(String sql, Pagination page, Dialect dialect) {
    	// 判断Pagination为空,直接返回SQL
    	if(null == page){
    		return sql;
    	}
    	// 判断Dialect为空,直接返回SQL
    	if(null == dialect){
    		return sql;
    	}
    	// 是否支持分页
        if (dialect.supportsLimit()) {
            int pageSize = page.getPageSize();
            int start = ((page.getPageNo() < 1) ? 1 : page.getPageNo() - 1) * pageSize;
            return dialect.getLimitString(sql, start, pageSize);
        } else {
            return sql;
        }
    }

    /**
     * 对SQL参数(?)进行设值,参考org.apache.ibatis.executor.parameter.DefaultParameterHandler.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            Configuration configuration = mappedStatement.getConfiguration();
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            MetaObject metaObject = parameterObject == null ? null :
                    configuration.newMetaObject(parameterObject);
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    PropertyTokenizer prop = new PropertyTokenizer(propertyName);
                    if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) {
                        value = boundSql.getAdditionalParameter(prop.getName());
                        if (value != null) {
                            value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
                        }
                    } else {
                        value = metaObject == null ? null : metaObject.getValue(propertyName);
                    }
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    if (typeHandler == null) {
                        throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
                    }
                    typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
                }
            }
        }
    }

}
